iT邦幫忙

2025 iThome 鐵人賽

DAY 18
0
Modern Web

用 Effect 實現產品級軟體系列 第 18

[學習 Effect Day18] Effect 進階錯誤管理 (四)

  • 分享至 

  • xImage
  •  

Effect.disconnect:讓 uninterruptible Effect 逾時時先回應、後收尾

在 Effect 中,Effect.disconnect 讓不可被中斷的工作在背景繼續執行。且不阻擋外部逾時的發生。使外部流程會在逾時當下先拿到逾時的錯誤結果,讓流程可以繼續走下去。

範例程式碼

我們有一個需要 2 秒才能完成的工作,但我們希望它能在 1 秒內超時。這樣我們才能測試Effect.disconnect的效果。

import { Effect } from "effect";

const formatTime = (date: Date) => {
  return date.toLocaleString("zh-TW", {
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
    hour: "2-digit",
    minute: "2-digit",
    second: "2-digit",
    hour12: false
  })
}

const longRunningTask = () =>
  Effect.gen(function*() {
    console.log(`[斷線示範] 開始工作 (${formatTime(new Date())})`)
    yield* Effect.sleep("2 seconds")
    console.log(`[斷線示範] 工作完成(在背景收尾) (${formatTime(new Date())})`)
  }).pipe(Effect.uninterruptible) // 關鍵:讓任務不可中斷

這裡要注意的點是,我們有使用 Effect.uninterruptible 來讓任務不可中斷。因為Effect.disconnect 是需要與 Effect.uninterruptible 一起使用才能發揮作用的。

我們要測試兩種情境:

  • 第一個是在運行不可中斷的工作時,沒有使用 Effect.disconnect。觀察結果是否會被阻擋住,直到工作完成,才拿到逾時的錯誤結果。
  • 第二個是在運行不可中斷的工作時,有使用 Effect.disconnect。觀察結果是否會在逾時當下先拿到逾時的錯誤結果,讓流程可以繼續走下去。

我們直接來看看程式碼的測試結果吧!

沒有 Effect.disconnect 的測試結果

// 測試 1: 沒有 Effect.disconnect
console.log("=== 測試 1: 沒有 Effect.disconnect ===")
const exit1 = await Effect.runPromiseExit(
  longRunningTask().pipe(Effect.timeout("1 second"))
)
console.log("結果:", exit1._tag)

// 等待一下看看是否有背景任務
console.log("等待 3 秒看是否有背景任務...")
await new Promise((resolve) => setTimeout(resolve, 3000))
/*
輸出:
[斷線示範] 開始工作 (2025/09/29 02:58:46)
[斷線示範] 工作完成(在背景收尾) (2025/09/29 02:58:48)
結果: Failure
等待 3 秒看是否有背景任務..
*/

使用 Effect.disconnect 的測試結果

// 測試 2: 有 Effect.disconnect
console.log("\n=== 測試 2: 有 Effect.disconnect ===")
const timedEffect = longRunningTask().pipe(
  Effect.disconnect, // 關鍵:允許工作在背景完成
  Effect.timeout("1 second")
)
const exit2 = await Effect.runPromiseExit(timedEffect)
console.log("結果:", exit2._tag)

// 等待一下讓背景任務完成
console.log("等待 3 秒讓背景任務完成...")
await new Promise((resolve) => setTimeout(resolve, 3000))
/*
輸出:
[斷線示範] 開始工作 (2025/09/29 02:59:25)
結果: Failure
等待 3 秒讓背景任務完成...
[斷線示範] 工作完成(在背景收尾) (2025/09/29 02:59:27)
*/

測試結果觀察

測試情境 使用 Effect.disconnect 執行時間 結果取得時機 背景任務完成 觀察重點
測試 1 ❌ 沒有使用 2 秒後 工作完成後才取得逾時錯誤 無背景任務 被不可中斷任務阻擋,直到完成才回傳逾時錯誤
測試 2 ✅ 有使用 1 秒後 逾時當下立即取得錯誤 有背景任務繼續執行 外部流程不被阻擋,背景任務繼續收尾

語意化分支:timeoutTo

Effect.timeoutTo 比 Effect.timeout 更靈活,它允許你為成功和超時兩種情況定義不同的結果。這在你想根據操作是否在時間內完成來客製化結果時非常有用。

語法結構

Effect.timeoutTo({
  duration: Duration,           // 超時時間
  onSuccess: (result) => T,     // 成功時的回調函數
  onTimeout: () => T            // 超時時的回調函數
})

範例程式碼

import { Effect, Either } from "effect"

const task = Effect.gen(function*() {
  console.log("Start processing...")
  yield* Effect.sleep("2 seconds") // Simulates a delay in processing
  console.log("Processing complete.")
  return "Result"
})

const program = task.pipe(
  Effect.timeoutTo({
    duration: "1 second",
    onSuccess: (result) => Either.right(result),
    onTimeout: () => Either.left("Timed out!")
  })
)

Effect.runPromise(program).then(console.log)
/**
 * 輸出:
 * Start processing...
 * { _id: 'Either', _tag: 'Left', left: 'Timed out!'}
 *
*/

與 Effect.timeout 的比較

特性 Effect.timeout Effect.timeoutTo
超時處理 拋出 TimeoutException 自定義超時結果
成功處理 返回原始結果 自定義成功結果
靈活性 較低 較高
使用場景 簡單超時控制 複雜業務邏輯

結論: Effect.timeoutTo 是處理需要根據超時狀態提供不同結果的場景的最佳選擇!

Day17-Day18 總結

核心 API 對比

API 逾時行為 適用場景
timeout 拋出 TimeoutException 簡單逾時控制
timeoutOption 返回 None 可選性操作
timeoutTo 自定義結果 複雜業務邏輯

關鍵概念

  • 可中斷任務:逾時時立即取消,適合快速響應
  • 不可中斷任務:不會被逾時中斷,適合關鍵操作
  • Effect.disconnect:外部流程可以在逾時當下先拿到逾時的錯誤結果,讓流程可以繼續走下去。
  • 數據驅動設定:使用 p99 延遲 + 緩衝設定合理逾時時間

參考資料


上一篇
[學習 Effect Day17] Effect 進階錯誤管理 (三)
下一篇
[學習 Effect Day19] Effect 進階錯誤管理 (五)
系列文
用 Effect 實現產品級軟體22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言